/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import io.sentry.exception.SentryEnvelopeException;
import io.sentry.protocol.SentryTransaction;
import io.sentry.util.Objects;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.concurrent.Callable;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * SentryEnvelopeItem
 *
 * @since 2021-06-02
 */
@ApiStatus.Internal
public final class SentryEnvelopeItem {
    @SuppressWarnings("CharsetObjectCanBeUsed")
    private static final Charset UTF_8 = Charset.forName("UTF-8");
    private final SentryEnvelopeItemHeader header;
    private final @Nullable Callable<byte[]> dataFactory;
    private @Nullable byte[] data;

    SentryEnvelopeItem(final @NotNull SentryEnvelopeItemHeader header, final byte[] data) {
        this.header = Objects.requireNonNull(header, "SentryEnvelopeItemHeader is required.");
        this.data = data;
        this.dataFactory = null;
    }

    SentryEnvelopeItem(
        final @NotNull SentryEnvelopeItemHeader header,
        final @Nullable Callable<byte[]> dataFactory) {
        this.header = Objects.requireNonNull(header, "SentryEnvelopeItemHeader is required.");
        this.dataFactory = Objects.requireNonNull(dataFactory, "DataFactory is required.");
        this.data = null;
    }

    /**
     * dataFactory is a Callable which returns theoretically a nullable result. Our implementations
     * always provide non-null values.
     *
     * @return data
     * @throws Exception
     */
    @SuppressWarnings("NullAway")
    public @NotNull byte[] getData() throws Exception {
        if (data == null && dataFactory != null) {
            data = dataFactory.call();
        }
        return data.clone();
    }

    public @NotNull SentryEnvelopeItemHeader getHeader() {
        return header;
    }

    /**
     * create SentryEnvelopeItem from session
     *
     * @param serializer serializer
     * @param session session
     * @return SentryEnvelopeItem
     * @throws IOException e
     */
    public static @NotNull SentryEnvelopeItem fromSession(
        final @NotNull ISerializer serializer, final @NotNull Session session) throws IOException {
        Objects.requireNonNull(serializer, getSerializerIsRequired());
        Objects.requireNonNull(session, "Session is required.");

        final CachedItem cachedItem =
            new CachedItem(
                () -> {
                    try (ByteArrayOutputStream stream = new ByteArrayOutputStream(); Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
                        serializer.serialize(session, writer);
                        return stream.toByteArray();
                    }
                });

        SentryEnvelopeItemHeader itemHeader =
            new SentryEnvelopeItemHeader(
                SentryItemType.Session, () -> cachedItem.getBytes(UTF_8).length, getContentType(), null);

        // Don't use method reference. This can cause issues on Ohos
        return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes(UTF_8));
    }

    @NotNull
    private static String getContentType() {
        return "application/json";
    }

    @NotNull
    private static String getSerializerIsRequired() {
        return "ISerializer is required.";
    }

    /**
     * getEvent
     *
     * @param serializer ISerializer
     * @return SentryEvent
     * @throws Exception e
     */
    public @Nullable SentryEvent getEvent(final @NotNull ISerializer serializer) throws Exception {
        if (header == null || header.getType() != SentryItemType.Event) {
            return null;
        }
        try (Reader eventReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(getData()), UTF_8))) {
            return serializer.deserialize(eventReader, SentryEvent.class);
        }
    }

    /**
     * SentryEnvelopeItem
     *
     * @param serializer ISerializer
     * @param event SentryBaseEvent
     * @return SentryEnvelopeItem
     * @throws IOException e
     */
    public static @NotNull SentryEnvelopeItem fromEvent(
        final @NotNull ISerializer serializer, final @NotNull SentryBaseEvent event)
        throws IOException {
        Objects.requireNonNull(serializer, getSerializerIsRequired());
        Objects.requireNonNull(event, "SentryEvent is required.");

        final CachedItem cachedItem =
            new CachedItem(
                () -> {
                    try (ByteArrayOutputStream stream = new ByteArrayOutputStream(); Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
                        serializer.serialize(event, writer);
                        return stream.toByteArray();
                    }
                });

        SentryEnvelopeItemHeader itemHeader =
            new SentryEnvelopeItemHeader(
                SentryItemType.resolve(event),
                () -> cachedItem.getBytes(UTF_8).length,
                getContentType(),
                null);

        // Don't use method reference. This can cause issues on Ohos
        return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes(UTF_8));
    }

    /**
     * SentryTransaction
     *
     * @param serializer ISerializer
     * @return SentryTransaction
     * @throws Exception e
     */
    public @Nullable SentryTransaction getTransaction(final @NotNull ISerializer serializer)
        throws Exception {
        if (header == null || header.getType() != SentryItemType.Transaction) {
            return null;
        }
        try (Reader eventReader =
                 new BufferedReader(new InputStreamReader(new ByteArrayInputStream(getData()), UTF_8))) {
            return serializer.deserialize(eventReader, SentryTransaction.class);
        }
    }

    /**
     * SentryEnvelopeItem
     *
     * @param serializer ISerializer
     * @param userFeedback UserFeedback
     * @return SentryEnvelopeItem
     */
    public static SentryEnvelopeItem fromUserFeedback(
        final @NotNull ISerializer serializer, final @NotNull UserFeedback userFeedback) {
        Objects.requireNonNull(serializer, getSerializerIsRequired());
        Objects.requireNonNull(userFeedback, "UserFeedback is required.");

        final CachedItem cachedItem =
            new CachedItem(
                () -> {
                    try (ByteArrayOutputStream stream = new ByteArrayOutputStream();
                         Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) {
                        serializer.serialize(userFeedback, writer);
                        return stream.toByteArray();
                    }
                });

        SentryEnvelopeItemHeader itemHeader =
            new SentryEnvelopeItemHeader(
                SentryItemType.UserFeedback,
                () -> cachedItem.getBytes(UTF_8).length,
                getContentType(),
                null);
        return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes(UTF_8));
    }

    /**
     * SentryEnvelopeItem from attachment
     *
     * @param attachment Attachment
     * @param maxAttachmentSize maxAttachmentSize
     * @return SentryEnvelopeItem
     */
    public static SentryEnvelopeItem fromAttachment(
        final @NotNull Attachment attachment, final long maxAttachmentSize) {
        final CachedItem cachedItem =
            new CachedItem(
                () -> {
                    if (attachment.getBytes(UTF_8) != null) {
                        if (attachment.getBytes(UTF_8).length > maxAttachmentSize) {
                            throw new SentryEnvelopeException(
                                String.format(
                                    "Dropping attachment with filename '%s', because the "
                                        + "size of the passed bytes with %d bytes is bigger "
                                        + "than the maximum allowed attachment size of "
                                        + "%d bytes.",
                                    attachment.getFilename(),
                                    attachment.getBytes(UTF_8).length,
                                    maxAttachmentSize));
                        }
                        return attachment.getBytes(UTF_8);
                    } else if (attachment.getPathname() != null) {
                        try {
                            checkAttachmentFile(attachment, maxAttachmentSize);
                            try (FileInputStream fileInputStream = new FileInputStream(attachment.getPathname());
                                 BufferedInputStream inputStream = new BufferedInputStream(fileInputStream);
                                 ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
                                byte[] bytes = new byte[1024];
                                int length;
                                int offset = 0;
                                while ((length = inputStream.read(bytes)) != -1) {
                                    outputStream.write(bytes, offset, length);
                                }
                                return outputStream.toByteArray();
                            }
                        } catch (IOException | SecurityException exception) {
                            throw new SentryEnvelopeException(
                                String.format(Locale.ROOT, "Reading the attachment %s failed.", attachment.getPathname()));
                        }
                    }
                    throw new SentryEnvelopeException(
                        String.format(
                            "Couldn't attach the attachment %s." + System.lineSeparator()
                                + "Please check that either bytes or a path is set.",
                            attachment.getFilename()));
                });

        SentryEnvelopeItemHeader itemHeader = new SentryEnvelopeItemHeader(
            SentryItemType.Attachment,
            () -> cachedItem.getBytes(UTF_8).length,
            attachment.getContentType(),
            attachment.getFilename());
        return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes(UTF_8));
    }

    private static void checkAttachmentFile(Attachment attachment, final long maxAttachmentSize) {
        File file = new File(attachment.getPathname());
        if (!file.isFile()) {
            try {
                throw new SentryEnvelopeException(
                    String.format(
                        "Reading the attachment %s failed, "
                            + "because the file located at the path is not a file.",
                        attachment.getPathname()));
            } catch (SentryEnvelopeException e) {
                e.printStackTrace();
            }
        }
        if (!file.canRead()) {
            try {
                throw new SentryEnvelopeException(
                    String.format(
                        "Reading the attachment %s failed, because can't read the file.",
                        attachment.getPathname()));
            } catch (SentryEnvelopeException e) {
                e.printStackTrace();
            }
        }
        if (file.length() > maxAttachmentSize) {
            try {
                throw new SentryEnvelopeException(
                    String.format(
                        "Dropping attachment, because the size of the it located at "
                            + "'%s' with %d bytes is bigger than the maximum "
                            + "allowed attachment size of %d bytes.",
                        attachment.getPathname(), file.length(), maxAttachmentSize));
            } catch (SentryEnvelopeException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * CachedItem
     *
     * @since 2021-06-02
     */
    private static class CachedItem {
        private @Nullable byte[] bytes;
        private final @Nullable Callable<byte[]> dataFactory;

        /**
         * constructor
         *
         * @param dataFactory dataFactory
         */
        CachedItem(final @Nullable Callable<byte[]> dataFactory) {
            this.dataFactory = dataFactory;
        }

        /**
         * getBytes
         *
         * @param charset Charset
         * @return byte[]
         * @throws Exception
         */
        public byte[] getBytes(Charset charset) throws Exception {
            if (bytes == null && dataFactory != null) {
                bytes = dataFactory.call();
            }
            return orEmptyArray(bytes);
        }

        private static @NotNull byte[] orEmptyArray(final @Nullable byte[] bytes) {
            return bytes != null ? bytes : new byte[]{};
        }
    }
}
